/*
 *  This file is part of the Image Metadata Editor.
 *
 *  Copyright (C) 2015 Australian Institute of Marine Science
 *
 *  Contact: Gael Lafond <g.lafond@aims.org.au>
 *
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 */
package au.gov.aims.exif.editor;

import au.gov.aims.exif.tools.ImageConverter;
import javafx.application.Application;
import javafx.collections.FXCollections;
import javafx.collections.ObservableList;
import javafx.event.ActionEvent;
import javafx.event.EventHandler;
import javafx.geometry.Insets;
import javafx.scene.Scene;
import javafx.scene.control.ContextMenu;
import javafx.scene.control.ListView;
import javafx.scene.control.Menu;
import javafx.scene.control.MenuBar;
import javafx.scene.control.MenuItem;
import javafx.scene.input.DragEvent;
import javafx.scene.input.Dragboard;
import javafx.scene.input.TransferMode;
import javafx.scene.layout.BorderPane;
import javafx.scene.layout.ColumnConstraintsBuilder;
import javafx.scene.layout.GridPane;
import javafx.scene.layout.Priority;
import javafx.scene.layout.VBox;
import javafx.stage.DirectoryChooser;
import javafx.stage.FileChooser;
import javafx.stage.Stage;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVStrategy;

import java.io.BufferedWriter;
import java.io.File;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.IOException;
import java.io.OutputStreamWriter;
import java.io.Reader;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.regex.Pattern;

// TODO Implement XLS import / export

public class CSVEditor extends Application {
	private static final Logger LOGGER = Logger.getLogger(CSVEditor.class.getName());
	private static final String ABOUT_PAGE = "http://eatlas.org.au/tools/image-metadata-editor#about";
	private static final String HELP_PAGE = "http://eatlas.org.au/tools/image-metadata-editor#help";

	private static final String CSV_SEPARATOR = ",";
	private static final String CSV_STR_DELIMITER = "\"";

	private static final String CSV_FILENAME_LABEL = "Filename";
	private static final String CSV_FILENAME_DESCRIPTION = "Name of the image.";
	private static final String CSV_COMMENT_PREFIX = "#";

	private static final CSVStrategy EXCEL_STRATEGY = new CSVStrategy(
			',', '"', CSVStrategy.COMMENTS_DISABLED, CSVStrategy.ESCAPE_DISABLED,
			true, true, false, true);
	// Common CSV used by OpenOffice and LibreOffice
	private static final CSVStrategy OOFFICE_STRATEGY = new CSVStrategy(
			';', '"', CSVStrategy.COMMENTS_DISABLED, CSVStrategy.ESCAPE_DISABLED,
			true, true, false, true);

	private static final List<CSVStrategy> CSV_STRATEGIES = new ArrayList<CSVStrategy>();
	static {
		CSV_STRATEGIES.add(CSVStrategy.DEFAULT_STRATEGY);
		CSV_STRATEGIES.add(CSVEditor.OOFFICE_STRATEGY);
		CSV_STRATEGIES.add(CSVEditor.EXCEL_STRATEGY);
		CSV_STRATEGIES.add(CSVStrategy.EXCEL_STRATEGY);
		CSV_STRATEGIES.add(CSVStrategy.TDF_STRATEGY);
	}

	private List<ExifWrapper> exifWrappers = null;

	// Fields
	private MenuItem saveMenuItem = null;
	private ListView<String> loadedFilesListView = null;

	private File fileChooserFolder = null;

	public static void main(String[] args) {
		launch(args);
	}

	@Override
	public void start(final Stage primaryStage) {
		primaryStage.setTitle(FXUtils.getWindowTitle("Batch editing"));
		primaryStage.getIcons().add(FXUtils.getResourceImage("icon-batch.png"));
		primaryStage.setMinWidth(450);
		primaryStage.setMinHeight(200);
		primaryStage.setWidth(800);
		primaryStage.setHeight(600);


		MenuBar menuBar = new MenuBar();

		// #############
		// # FILE MENU #
		// #############

		Menu menuFile = new Menu("File");
		// Menu item with an icon:
		// MenuItem shuffle = new MenuItem("Shuffle", new ImageView(new Image("menusample/new.png")));

		MenuItem addImage = new MenuItem("Add image...");
		addImage.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final FileChooser fileChooser = new FileChooser();
				fileChooser.setTitle("Select the images to add to the list");
				if (CSVEditor.this.fileChooserFolder != null) {
					fileChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}
				List<File> chosenFiles = fileChooser.showOpenMultipleDialog(primaryStage);
				if (chosenFiles != null) {
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(chosenFiles);
					CSVEditor.this.addFiles(chosenFiles);
				}
			}
		});
		menuFile.getItems().addAll(addImage);

		MenuItem addFolder = new MenuItem("Add folder...");
		addFolder.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final DirectoryChooser folderChooser = new DirectoryChooser();
				folderChooser.setTitle("Select the folder to add to the list");
				if (CSVEditor.this.fileChooserFolder != null) {
					folderChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}
				File folder = folderChooser.showDialog(primaryStage);
				if (folder != null) {
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(folder);
					CSVEditor.this.addFile(folder);
				}
			}
		});
		menuFile.getItems().addAll(addFolder);

		MenuItem sort = new MenuItem("Sort");
		sort.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				CSVEditor.this.sort();
			}
		});
		menuFile.getItems().addAll(sort);

		MenuItem clear = new MenuItem("Clear");
		clear.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				CSVEditor.this.clear();
			}
		});
		menuFile.getItems().addAll(clear);

		this.saveMenuItem = new MenuItem("Save EXIF to images");
		this.saveMenuItem.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				// initialize the confirmation dialog
				new FXUtils.ConfirmWindow(
						primaryStage,
						"Modifying files",
						"The application will modify all listed files with the values provided.\nAre you sure you want to continue?",
						300) {
					@Override
					public void handle(ActionEvent actionEvent) {
						// take action and close the dialog.
						CSVEditor.this.save();
					}
				}.show();
			}
		});
		this.saveMenuItem.setDisable(true);
		menuFile.getItems().addAll(this.saveMenuItem);

		MenuItem quit = new MenuItem("Quit");
		quit.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				primaryStage.close();
			}
		});
		menuFile.getItems().addAll(quit);


		// ########################
		// # IMPORT / EXPORT MENU #
		// ########################

		Menu menuImportExport = new Menu("Import / Export");

		MenuItem importCsv = new MenuItem("Import EXIF from CSV...");
		importCsv.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final FileChooser fileChooser = new FileChooser();
				fileChooser.setTitle("Select the CSV file to import");
				if (CSVEditor.this.fileChooserFolder != null) {
					fileChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}

				// Set extension filter
				FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("CSV files (*.csv)", "*.csv");
				fileChooser.getExtensionFilters().add(extFilter);

				// Ask for input file
				File csvFile = fileChooser.showOpenDialog(primaryStage);
				if (csvFile != null) {
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(csvFile);
					try {
						CSVEditor.this.importCsv(csvFile);
					} catch (Exception ex) {
						FXUtils.getNotificationWindow().showError("Invalid CSV file. Try to edit it with spreadsheet application like Excel, and save it as CSV file.");
						LOGGER.log(Level.SEVERE, "Invalid CSV file", ex);
					}
				}
			}
		});
		menuImportExport.getItems().addAll(importCsv);

		MenuItem importXls = new MenuItem("Import EXIF from XLS...");
		importXls.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final FileChooser fileChooser = new FileChooser();
				fileChooser.setTitle("Select the XLS file to import");
				if (CSVEditor.this.fileChooserFolder != null) {
					fileChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}

				// Set extension filter
				FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("XLS files (*.xls or *.xlsx)", "*.xls", "*.xlsx");
				fileChooser.getExtensionFilters().add(extFilter);

				// Ask for input file
				File xlsFile = fileChooser.showOpenDialog(primaryStage);
				if (xlsFile != null) {
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(xlsFile);
					try {
						CSVEditor.this.importXls(xlsFile);
					} catch (Exception ex) {
						FXUtils.getNotificationWindow().showError("Invalid XLS file. Try to save it with spreadsheet application like Excel, or send the file to the e-Atlas team.");
						LOGGER.log(Level.SEVERE, "Invalid XLS file", ex);
					}
				}
			}
		});
		// TODO Remove this line after implementing XLS import
		importXls.setDisable(true);
		menuImportExport.getItems().addAll(importXls);

		MenuItem exportCsv = new MenuItem("Export EXIF to CSV...");
		exportCsv.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final FileChooser fileChooser = new FileChooser();
				fileChooser.setTitle("Save CSV file");
				if (CSVEditor.this.fileChooserFolder != null) {
					fileChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}

				// Set extension filter
				FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("CSV files (*.csv)", "*.csv");
				fileChooser.getExtensionFilters().add(extFilter);

				// Ask for output file
				// Show save file dialog
				File csvFile = fileChooser.showSaveDialog(primaryStage);
				if (csvFile != null) {
					if (!csvFile.getPath().toLowerCase().endsWith(".csv")) {
						csvFile = new File(csvFile.getAbsolutePath() + ".csv");
					}
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(csvFile);
					try {
						CSVEditor.this.exportCsv(csvFile);
					} catch (Exception ex) {
						FXUtils.getNotificationWindow().showError("Can not export the images metadata to CSV");
						LOGGER.log(Level.SEVERE, "Can not export the images metadata to CSV", ex);
					}
				}
			}
		});
		menuImportExport.getItems().addAll(exportCsv);

		MenuItem exportXls = new MenuItem("Export EXIF to XLS...");
		exportXls.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				final FileChooser fileChooser = new FileChooser();
				fileChooser.setTitle("Save XLS file");
				if (CSVEditor.this.fileChooserFolder != null) {
					fileChooser.setInitialDirectory(CSVEditor.this.fileChooserFolder);
				}

				// Set extension filter
				FileChooser.ExtensionFilter extFilter = new FileChooser.ExtensionFilter("XLS files (*.xls)", "*.xls", "*.xlsx");
				fileChooser.getExtensionFilters().add(extFilter);

				// Ask for output file
				// Show save file dialog
				File xlsFile = fileChooser.showSaveDialog(primaryStage);
				if (xlsFile != null) {
					// Remember the folder to re-open the file chooser at the same location
					CSVEditor.this.setFileChooserFolder(xlsFile);
					try {
						CSVEditor.this.exportXls(xlsFile);
					} catch(Exception ex) {
						FXUtils.getNotificationWindow().showError("Can not export the images metadata to XLS");
						LOGGER.log(Level.SEVERE, "Can not export the images metadata to XLS", ex);
					}
				}
			}
		});
		// TODO Remove this line after implementing XLS export
		exportXls.setDisable(true);
		menuImportExport.getItems().addAll(exportXls);


		// ##############
		// # TOOLS MENU #
		// ##############

		Menu menuTools = new Menu("Tools");

		MenuItem convertImages = new MenuItem("Convert image files to JPEG");
		convertImages.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				ImageConverter converter = new ImageConverter();
				converter.start(new Stage());
			}
		});
		menuTools.getItems().addAll(convertImages);


		// #############
		// # HELP MENU #
		// #############

		Menu menuHelp = new Menu("Help");

		MenuItem about = new MenuItem("About");
		about.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				CSVEditor.this.getHostServices().showDocument(ABOUT_PAGE);
			}
		});
		menuHelp.getItems().addAll(about);

		MenuItem help = new MenuItem("Help");
		help.setOnAction(new EventHandler<ActionEvent>() {
			@Override
			public void handle(ActionEvent event) {
				CSVEditor.this.getHostServices().showDocument(HELP_PAGE);
			}
		});
		menuHelp.getItems().addAll(help);


		menuBar.getMenus().addAll(menuFile, menuImportExport, menuTools, menuHelp);



		BorderPane body = new BorderPane();

		// Put the menu and the application header in the top of the border panel
		VBox vbox = new VBox();
		vbox.getChildren().addAll(menuBar, FXUtils.createApplicationHeader());
		body.setTop(vbox);

		GridPane grid = new GridPane();
		grid.getColumnConstraints().addAll(
				ColumnConstraintsBuilder.create().hgrow(Priority.ALWAYS).build());
		grid.setPadding(new Insets(10, 10, 10, 10));
		grid.setVgap(5);
		grid.setHgap(5);

		int row = 0;
		this.loadedFilesListView = new ListView<String>();
		this.setContextualMenu(this.loadedFilesListView);
		GridPane.setConstraints(this.loadedFilesListView, 0, row, 2, 1);
		GridPane.setVgrow(this.loadedFilesListView, Priority.ALWAYS);
		grid.getChildren().add(this.loadedFilesListView);

		body.setCenter(grid);

		Scene scene = new Scene(body);

		// Drag and drop file to the editor (this is way easier that the file chooser)
		scene.setOnDragOver(new EventHandler<DragEvent>() {
			@Override
			public void handle(DragEvent event) {
				Dragboard db = event.getDragboard();
				if (db.hasFiles()) {
					event.acceptTransferModes(TransferMode.COPY);
				} else {
					event.consume();
				}
			}
		});
		scene.setOnDragDropped(new EventHandler<DragEvent>() {
			@Override
			public void handle(DragEvent event) {
				Dragboard db = event.getDragboard();
				boolean success = false;
				if (db.hasFiles()) {
					List<File> chosenFiles = db.getFiles();
					if (chosenFiles != null && !chosenFiles.isEmpty()) {
						success = true;

						// Remember the folder to re-open the file chooser at the same location
						CSVEditor.this.setFileChooserFolder(chosenFiles);

						// If there is only one file and the file is a CSV file, load the CSV
						boolean containsNoImage = false; // True if we know for sure that the array contains no image.
						if (chosenFiles.size() == 1) {
							File csvFile = chosenFiles.get(0);
							if (!csvFile.isDirectory()) {
								try {
									MagicNumber magicNumber = MagicNumber.getMagicNumber(csvFile);
									if (magicNumber == null) {
										containsNoImage = true;
										// Can not detect the file type, just assume it's a text file, probably a CSV...
										try {
											CSVEditor.this.importCsv(csvFile);
										} catch(Exception ex) {
											FXUtils.getNotificationWindow().showError("The file " + csvFile.getAbsolutePath() + " is not supported. " +
													"If you are trying to load a CSV file, try to edit it with spreadsheet application like Excel, and save it as CSV file.");
											LOGGER.log(Level.SEVERE, "Invalid CSV file", ex);
										}
									}
								} catch(Exception ex) {
									LOGGER.log(Level.WARNING, "Can not find the magic number of file " + csvFile.getAbsolutePath(), ex);
								}
							}
						}
						if (!containsNoImage) {
							CSVEditor.this.addFiles(chosenFiles);
						}
					}
				}
				event.setDropCompleted(success);
				event.consume();
			}
		});

		primaryStage.setScene(scene);
		primaryStage.show();
	}

	// http://fxexperience.com/2012/05/listview-custom-cell-factories-and-context-menus/
	private void setContextualMenu(final ListView<String> listView) {
		// Create a MenuItem and place it in a ContextMenu
		MenuItem remove = new MenuItem("Remove");
		remove.setOnAction(new EventHandler<ActionEvent>() {
			public void handle(ActionEvent e) {
				int selectedIndex = listView.getSelectionModel().getSelectedIndex();
				if (selectedIndex >= 0) {
					// NOTE: This will delete the selected row. The SelectionModel do not always select
					//     the row on right click. Sometime the user has to left-click before right-click
					//     (for example, when right-click a row while a context menu is already showing
					//     or just after deleting a row)
					CSVEditor.this.exifWrappers.remove(listView.getSelectionModel().getSelectedIndex());
					CSVEditor.this.displayLoadedFiles();
				}
			}
		});
		MenuItem edit = new MenuItem("Edit metadata");
		edit.setOnAction(new EventHandler<ActionEvent>() {
			public void handle(ActionEvent e) {
				ExifWrapper selectedExifWrapper = CSVEditor.this.exifWrappers.get(listView.getSelectionModel().getSelectedIndex());
				if (selectedExifWrapper != null) {
					ImageEditor editor = new ImageEditor();
					editor.start(new Stage(), selectedExifWrapper);
				}
			}
		});
		ContextMenu contextMenu = new ContextMenu(edit, remove);

		// sets a cell factory on the ListView telling it to use the previously-created ContextMenu and double click
		listView.setCellFactory(new FXUtils.ListViewCallback<String>(true, contextMenu){
			@Override
			public void onDoubleClick() {
				ExifWrapper selectedExifWrapper = CSVEditor.this.exifWrappers.get(listView.getSelectionModel().getSelectedIndex());
				if (selectedExifWrapper != null) {
					ImageEditor editor = new ImageEditor();
					editor.start(new Stage(), selectedExifWrapper);
				}
			}
		});
	}

	private void setFileChooserFolder(File chosenFile) {
		if (chosenFile != null) {
			File folder = chosenFile.getParentFile();
			if (folder != null) {
				this.fileChooserFolder = folder;
			}
		}
	}
	private void setFileChooserFolder(List<File> chosenFiles) {
		if (chosenFiles != null && !chosenFiles.isEmpty()) {
			this.setFileChooserFolder(chosenFiles.get(0));
		}
	}

	private void addFile(File chosenFile) {
		List<File> chosenFiles = new ArrayList<File>();
		chosenFiles.add(chosenFile);
		this.addFiles(chosenFiles);
	}

	// NOTE: On folder load, do not show errors for incompatible files. Only show errors for JPEG that could not be loaded
	private void addFiles(List<File> chosenFiles) {
		this.addFiles(chosenFiles, true);
	}
	private void addFiles(List<File> chosenFiles, boolean showUnsupportedFileErrors) {
		if (chosenFiles != null) {
			if (this.exifWrappers == null) {
				this.exifWrappers = new ArrayList<ExifWrapper>();
			}
			// Clear the list while the app load the files
			this.displayLoadedFiles();

			for (File chosenFile : chosenFiles) {
				if (chosenFile != null) {
					if (!chosenFile.exists()) {
						FXUtils.getNotificationWindow().showError("The file " + chosenFile.getAbsolutePath() + " do not exists.");
					} else if (!chosenFile.canRead()) {
						FXUtils.getNotificationWindow().showError("The application do not have sufficient privileges to read the file " + chosenFile.getAbsolutePath() + ".");
					} else if (chosenFile.isDirectory()) {
						// Recursive loading of folders
						File[] folderFiles = chosenFile.listFiles();
						if (folderFiles != null && folderFiles.length > 0) {
							this.addFiles(Arrays.asList(folderFiles), false);
						}
					} else if (!ExifWrapper.isSupportedImage(chosenFile)) {
						if (showUnsupportedFileErrors) {
							FXUtils.getNotificationWindow().showError("The file " + chosenFile.getAbsolutePath() + " is not supported.");
						}
					} else {
						ExifWrapper exifWrapper = new ExifWrapper(chosenFile);
						if (exifWrapper.isEmpty()) {
							// The image is a JPEG but it doesn't have an EXIF (and the app could not create one)
							// This is very unlikely to happen.
							FXUtils.getNotificationWindow().showError("The file " + chosenFile.getAbsolutePath() + " is not supported.");
						} else {
							// Validate it's GPS value
							exifWrapper.getGPSValue();
							this.exifWrappers.add(exifWrapper);
						}
					}
				}
			}
			this.saveMenuItem.setDisable(true);
			this.displayLoadedFiles();
		}
	}

	private void sort() {
		Collections.sort(this.exifWrappers, new ExifWrapper.ExifWrapperComparator());
		this.displayLoadedFiles();
	}

	private void clear() {
		this.exifWrappers = new ArrayList<ExifWrapper>();
		this.displayLoadedFiles();
	}

	public void displayLoadedFiles() {
		if (this.exifWrappers == null) {
			this.loadedFilesListView.setItems(null);
		} else {
			ObservableList<String> items = FXCollections.observableArrayList();
			for (ExifWrapper exifWrapper : this.exifWrappers) {
				items.add(exifWrapper.getImageFile().getAbsolutePath());
			}
			this.loadedFilesListView.setItems(items);
		}
	}

	public void exportXls(File xlsFile) throws IOException {
		// TODO
		throw new UnsupportedOperationException();
	}

	public void exportCsv(File csvFile) throws IOException {
		BufferedWriter writer = null;
		try {
			writer = new BufferedWriter(new OutputStreamWriter(new FileOutputStream(csvFile), "UTF-8"));
			this.writeCsvLabels(writer);
			this.writeCsvDescriptions(writer);
			for (ExifWrapper exifWrapper : this.exifWrappers) {
				if (exifWrapper != null) {
					this.writeCsvRow(csvFile.getParentFile(), writer, exifWrapper);
				}
			}
		} finally {
			if (writer != null) {
				try {
					writer.flush();
				} catch(Exception ex) {
					FXUtils.getNotificationWindow().showWarning("Can not flush the CSV writer");
					LOGGER.log(Level.SEVERE, "Can not flush the CSV writer", ex);
				}
				try {
					writer.close();
				} catch(Exception ex) {
					FXUtils.getNotificationWindow().showWarning("Can not close the CSV writer");
					LOGGER.log(Level.SEVERE, "Can not close the CSV writer", ex);
				}
			}
		}
	}

	private void writeCsvLabels(BufferedWriter writer) throws IOException {
		writer.write(this.csvSafeString(CSV_FILENAME_LABEL));
		writer.write(CSV_SEPARATOR);
		for (EAtlasField field : EAtlasField.values()) {
			writer.write(this.csvSafeString(field.getCsvLabel()));
			writer.write(CSV_SEPARATOR);
		}
		writer.newLine();
	}

	private void writeCsvDescriptions(BufferedWriter writer) throws IOException {
		writer.write(this.csvSafeString(CSV_COMMENT_PREFIX + " " +  CSV_FILENAME_DESCRIPTION));
		writer.write(CSV_SEPARATOR);
		for (EAtlasField field : EAtlasField.values()) {
			writer.write(this.csvSafeString(field.getCsvDescription()));
			writer.write(CSV_SEPARATOR);
		}
		writer.newLine();
	}

	private void writeCsvRow(File sourceFolder, BufferedWriter writer, ExifWrapper exifWrapper) throws IOException {
		// First column: Filename
		if (exifWrapper != null) {
			writer.write(this.csvSafeString(StringUtils.getFileRelativePath(sourceFolder, exifWrapper.getImageFile())));
			writer.write(CSV_SEPARATOR);

			Double[] gps = exifWrapper.getGPSValue();
			for (EAtlasField field : EAtlasField.values()) {
				if (EAtlasField.LONGITUDE.equals(field) || EAtlasField.LATITUDE.equals(field)) {
					if (gps != null) {
						int ind = EAtlasField.LONGITUDE.equals(field) ? ExifWrapper.LONGITUDE_IND : ExifWrapper.LATITUDE_IND;
						writer.write(this.csvSafeDouble(gps[ind]));
					}
				} else {
					writer.write(this.csvSafeString(field.getValue(exifWrapper)));
				}
				writer.write(CSV_SEPARATOR);
			}
		}

		writer.newLine();
	}

	private String csvSafeString(String str) {
		if (str == null) {
			return "";
		}
		// Some editor (like OOffice) has issues loading fields contains semi-column. It's safer to always "quote" the strings.
		return CSV_STR_DELIMITER + this.csvSafe(str) + CSV_STR_DELIMITER;
	}
	private String csvSafeDouble(Double value) {
		if (value == null) {
			return "";
		}
		return this.csvSafe(StringUtils.doubleToString(value));
	}
	private String csvSafe(String str) {
		if (str == null) {
			return "";
		}
		return str.replace(CSV_STR_DELIMITER, CSV_STR_DELIMITER+CSV_STR_DELIMITER);
	}

	public void importXls(File xlsFile) throws IOException {
		// TODO
		throw new UnsupportedOperationException();
	}

	public void importCsv(File csvFile) throws IOException {
		if (csvFile == null) {
			FXUtils.getNotificationWindow().showError("Invalid CSV file");
			LOGGER.log(Level.WARNING, "Invalid CSV file");
			return;
		}

		Reader reader = null;
		File currentFolder = csvFile.getParentFile();

		try {
			CSVParser csvParser = null;
			Map<String, Integer> header = null;
			// Verify if we can read the file header using one of the CSV Strategy
			// (the CSV file can use different characters for separator / string delimiter)
			for (CSVStrategy csvStrategy : CSV_STRATEGIES) {
				reader = new FileReader(csvFile);
				csvParser = new CSVParser(reader, csvStrategy);

				String[] headerArr = csvParser.getLine();
				header = new HashMap<String, Integer>();
				if (headerArr != null) {
					for (int i=0; i<headerArr.length; i++) {
						String label = StringUtils.trim(headerArr[i]);
						if (!StringUtils.isEmpty(label)) {
							// Fix for messed-up CSV file (MS Notepad add invisible chars that confuse the parser)
							if (label.contains(CSV_STR_DELIMITER)) {
								String quotedDelimiter = Pattern.quote(CSV_STR_DELIMITER);
								label = label.replaceAll("^.*" + quotedDelimiter +"(.+)" + quotedDelimiter + ".*$", "$1");
							}
							header.put(label, i);
						}
					}
				}
				if (!header.containsKey(CSV_FILENAME_LABEL)) {
					// The CSV Strategy is no good, try an other one...
					try {
						reader.close();
						reader = null;
					} catch(Exception ex) {
						FXUtils.getNotificationWindow().showWarning("Can not close the CSV file reader");
						LOGGER.log(Level.WARNING, "Can not close the CSV file reader", ex);
					}
				} else {
					// The CSV Strategy is good, let's parse that file!
					break;
				}
			}

			if (header == null || !header.containsKey(CSV_FILENAME_LABEL)) {
				throw new IOException("Invalid CSV file");
			} else {
				this.exifWrappers = new ArrayList<ExifWrapper>();
				this.displayLoadedFiles();

				// Read each line of the CSV, one by one
				String[] line;
				int fileNameIndex = header.get(CSV_FILENAME_LABEL);
				while ((line = csvParser.getLine()) != null) {
					// Ignore commented lines and line that do not have a filename
					if (!line[0].startsWith(CSV_COMMENT_PREFIX) && line[fileNameIndex] != null && !line[fileNameIndex].isEmpty()) {
						File file = StringUtils.getFileFromRelativePath(currentFolder, line[fileNameIndex]);
						if (!file.exists()) {
							FXUtils.getNotificationWindow().showError("Missing file " + file.getAbsolutePath());
							LOGGER.log(Level.WARNING, "Missing file {0}", file.getAbsolutePath());
						} else if (!file.canRead()) {
							FXUtils.getNotificationWindow().showError("File " + file.getAbsolutePath() + " is not readable");
							LOGGER.log(Level.WARNING, "File {0} is not readable", file.getAbsolutePath());
						} else if (!file.canWrite()) {
							FXUtils.getNotificationWindow().showError("File " + file.getAbsolutePath() + " is not writable");
							LOGGER.log(Level.WARNING, "File {0} is not writable", file.getAbsolutePath());
						} else {
							ExifWrapper exifWrapper = new ExifWrapper(file);
							if (exifWrapper.isEmpty()) {
								FXUtils.getNotificationWindow().showError("The file " + file.getAbsolutePath() + " is not supported.");
							} else {
								this.applyModifications(exifWrapper, header, line);
								this.exifWrappers.add(exifWrapper);
							}
						}
					}
				}
				this.displayLoadedFiles();
			}

		} finally {
			if (reader != null) {
				try {
					reader.close();
				} catch(Exception ex) {
					FXUtils.getNotificationWindow().showWarning("Can not close the CSV file reader");
					LOGGER.log(Level.WARNING, "Can not close the CSV file reader", ex);
				}
			}
		}

		if (CSVEditor.this.exifWrappers != null && !CSVEditor.this.exifWrappers.isEmpty()) {
			CSVEditor.this.saveMenuItem.setDisable(false);
		} else {
			CSVEditor.this.saveMenuItem.setDisable(true);
		}
	}

	private void applyModifications(ExifWrapper exifWrapper, Map<String, Integer> header, String[] values) {
		String gpsLon = null, gpsLat = null, gpsRadius = null;

		for (EAtlasField field : EAtlasField.values()) {
			Integer index = header.get(field.getCsvLabel());
			if (index != null && index < values.length) {
				String value = StringUtils.trim(values[index]);

				if (EAtlasField.LONGITUDE.equals(field)) {
					if (!StringUtils.isEmpty(value)) {
						gpsLon = value;
					}
				} else if (EAtlasField.LATITUDE.equals(field)) {
					if (!StringUtils.isEmpty(value)) {
						gpsLat = value;
					}
				} else if (EAtlasField.RADIUS.equals(field)) {
					if (!StringUtils.isEmpty(value)) {
						gpsRadius = value;
					}
				} else {
					field.setValue(exifWrapper, value);
				}
			}
		}

		if (gpsLon != null || gpsLat != null) {
			// Only modify the GPS values if they are valid
			if (ExifWrapper.preValidateGPS(gpsLon, gpsLat, gpsRadius)) {
				exifWrapper.setGPSValue(new Double[]{ Double.valueOf(gpsLon), Double.valueOf(gpsLat) });
				EAtlasField.RADIUS.setValue(exifWrapper, gpsRadius);
			}
		} else {
			exifWrapper.setGPSValue(null);
		}
	}

	private void save() {
		if (this.exifWrappers == null || this.exifWrappers.isEmpty()) {
			FXUtils.getNotificationWindow().showWarning("No loaded files. Load a CSV file prior to save the images metadata.");
		} else {
			int nbSaved = 0;
			for (ExifWrapper exifWrapper : this.exifWrappers) {
				try {
					// Only save the new values if they are valid
					if (exifWrapper.validate(true)) {
						exifWrapper.save();
						nbSaved++;
					}
				} catch (Exception ex) {
					FXUtils.getNotificationWindow().showError("Can not save the image " + exifWrapper.getImageFile().getAbsolutePath());
					LOGGER.log(Level.WARNING, "Can not save the image " + exifWrapper.getImageFile().getAbsolutePath(), ex);
				}
			}
			FXUtils.getNotificationWindow().showMessage(
				nbSaved > 0 ?
					nbSaved + " images saved" :
					nbSaved + " image saved"
			);
		}
	}
}
